// 
// Logger.cs
//  
// Author:
//       Tomas Vondracek <tomas.vondracek@gmail.com>
// 
// Copyright (c) 2009 Tomas Vondracek
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.



using System;
using System.Xml;
using System.Collections.Generic;
using System.Text;
using System.Diagnostics;
using System.Reflection;

namespace Logging
{
	public interface IConsole
	{
		void ConsoleWriteLine (String text);
	}

	public enum LogLevel
	{
		Debug = 0,
		Info = 1,
		Warning = 2,
		Error = 3,
		Fatal = 4
	}

	public class Logger
	{
		const string CSS = 
		"table { border-style:dotted; border-top-style:none; border-left-style:none;border-right-style:none;" +
		" border-color:Black; border-bottom-width: 1px; width:800px; }" +
		".info { color:black; }"+
		".important { color:Green; font-weight:bold; } "+
		".warning { color:Orange; }" +
		".error { color:Red; font-weight:bold; }" +
		".time { color:Blue; width:60px; }" + 
		".duration { color: Blue; }";
		static XmlDataDocument doc;
		static XmlDataDocument dbgDoc;

		private static XmlNode bodyNode;
		private static XmlNode dbgBodyNode;

		static DateTime startTime;
		static List<DateTime> times;
		static List<Stopwatch> stopwatches;
		static List<int> fpss;
		static bool bInitialized = false;

		/// <summary>
		/// if true, write messages to std output 
		/// </summary>
		private static bool bWriteToOutput = false;
		
		/// <summary>
		/// if true, write messages to attached console too 
		/// </summary>
		private static bool bWriteToConsole = true;
		
		/// <summary>
		/// global log level 
		/// </summary>
		private static LogLevel logLevel = LogLevel.Warning;

		static string savePath = "";
		
		private static PerformanceCounter ramCounter;

		public static bool IsInitialized {
			get { return Logger.bInitialized; }
		}

		static IConsole logConsole;

		/// <summary>
		/// attach console - messages will be passed to such console 
		/// </summary>
		/// <param name="c">
		/// A <see cref="IConsole"/>
		/// </param>
		public static void AttachConsole (IConsole c)
		{
			Logger.logConsole = c;
		}

		static Logger ()
		{
			InitLogger ();
		}

		/// <summary>
		/// logger initialization, it is not neccesary since static contructor is present 
		/// </summary>
		public static void InitLogger ()
		{
			if (IsInitialized)
			{
				return;
			}

#if DEBUG
			logLevel = LogLevel.Debug;
#else
			logLevel = LogLevel.Info;
#endif

#if DEBUG
			SavePath = string.Format ("{1}{0}logs{0}", System.IO.Path.DirectorySeparatorChar, System.Environment.CurrentDirectory);
#else
			SavePath = System.IO.Path.Combine (System.Environment.GetFolderPath (Environment.SpecialFolder.ApplicationData), "histogram");
			SavePath = System.IO.Path.Combine (savePath, "logs");			
#endif

            //CreateSavePath ();
            
			AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler (CurrentDomain_UnhandledException);

			startTime = DateTime.Now;
			times = new List<DateTime> ();
			fpss = new List<int> (1000);
			stopwatches = new List<Stopwatch> ();

			doc = new XmlDataDocument ();
			doc.CreateXmlDeclaration ("1.0", "UTF-8", "yes");

			dbgDoc = new XmlDataDocument ();
			dbgDoc.CreateXmlDeclaration ("1.0", "UTF-8", "yes");

			XmlElement html = doc.CreateElement ("html");
			XmlElement head = doc.CreateElement ("head");
			XmlElement link = doc.CreateElement ("link");
			XmlAttribute type = doc.CreateAttribute ("type");
			type.InnerText = "text/css";
			link.Attributes.Append (type);
			XmlAttribute rel = doc.CreateAttribute ("rel");
			rel.InnerText = "Stylesheet";
			link.Attributes.Append (rel);
			XmlAttribute href = doc.CreateAttribute ("href");
			href.InnerText = "logging.css";
			link.Attributes.Append (href);
			
			XmlElement cssElement = doc.CreateElement("style");
			cssElement.InnerText = CSS;
			XmlAttribute typeAtt = doc.CreateAttribute("type");
			typeAtt.InnerText = "text/css";

			head.AppendChild (link);
			head.AppendChild (cssElement);
			html.AppendChild (head);

			XmlElement body = doc.CreateElement ("body");
			XmlElement text = doc.CreateElement ("div");
			text.InnerXml = "Logger verze 0.4; cas zacatku: " + startTime.ToString ();
			body.AppendChild (text);

			html.AppendChild (body);
			doc.AppendChild (html);

			dbgDoc = (XmlDataDocument)doc.CloneNode (true);

			ramCounter = new PerformanceCounter("Memory", "Available MBytes");
			
			bodyNode = doc.GetElementsByTagName ("body")[0];
			dbgBodyNode = dbgDoc.GetElementsByTagName ("body")[0];



			bInitialized = true;
			Console.WriteLine ("Logger spusten");

			AddInternalInfo ("Executing Assembly version " + Logger.AssemblyVersion);
			AddInternalInfo ("Command " + System.Environment.CommandLine);
			AddInternalInfo ("Machine " + System.Environment.MachineName);
			AddInternalInfo ("OS" + System.Environment.OSVersion.ToString ());
			AddInternalInfo ("CPU count " + System.Environment.ProcessorCount.ToString ());
			AddInternalInfo ("RAM " + (100 * ramCounter.NextValue()).ToString());
			if (RuntimeIsMono == true)
			{
				AddInfo ("MONO runtime");
			}
			AddInternalInfo ("Runtime " + System.Environment.Version.ToString ());
			AddInternalInfo ("Username " + System.Environment.UserName);
			AddImportant ("LogLevel " + logLevel.ToString ());

			Save ();
			AddInfo ("-----------------------------------------");
		}
		
		/// <summary>
		/// creates a directory to save logs into
		/// </summary>
		/// <remarks>
		/// it uses local field savePath
		/// </remarks>
		protected static void CreateSavePath ()
		{
			try
            {
                if (!System.IO.Directory.Exists(savePath))
                {
                    System.IO.Directory.CreateDirectory(savePath);
                }
            }
            catch (System.IO.IOException e)
            {
                Console.WriteLine("Can't create directory for saving logs!\n" + e.ToString());
            }
            catch (System.Exception ex)
            {
                Console.WriteLine("Can't create directory for saving logs from unknown reason!\n" + ex.ToString());
            }
		}

		/// <summary>
		/// log the unhandled exception of the application 
		/// </summary>
		/// <param name="sender">
		/// A <see cref="System.Object"/>
		/// </param>
		/// <param name="e">
		/// A <see cref="UnhandledExceptionEventArgs"/>
		/// </param>
		static void CurrentDomain_UnhandledException (object sender, UnhandledExceptionEventArgs e)
		{
			AddError ("V aplikaci doslo k neodchycene vyjimce!" + e.ExceptionObject.ToString ());
		}

		/// <summary>
		/// the time in miliseconds from the application start
		/// </summary>
		public static int AppTime {
			get { return ((int)(DateTime.Now - startTime).TotalMilliseconds); }
		}

		private static void AddText (string message, string style)
		{
			AddText (message, style, false);
		}

		/// <summary>
		/// pass the message to log with given style
		/// </summary>
		/// <param name="message">
		/// A <see cref="System.String"/> message to log
		/// </param>
		/// <param name="style">
		/// A <see cref="System.String"/> style of message
		/// </param>
		/// <param name="isProblem">
		/// A <see cref="System.Boolean"/> - if true,
		/// message is logged to debug log too.
		/// </param>
		private static void AddText (string message, string style, bool isProblem)
		{
			XmlElement table = doc.CreateElement ("table");
			XmlElement tr = doc.CreateElement ("tr");
			XmlElement tdTime = doc.CreateElement ("td");
			XmlAttribute aTime = doc.CreateAttribute ("class");
			aTime.InnerText = "time";
			tdTime.Attributes.Append (aTime);
			tdTime.InnerText = AppTime.ToString ();
			XmlElement tdText = doc.CreateElement ("td");
			XmlAttribute aclass = doc.CreateAttribute ("class");
			aclass.InnerText = style;
			tdText.Attributes.Append (aclass);

			string encodedMessage = System.Web.HttpUtility.HtmlEncode (message);

			try
			{
				tdText.InnerXml = encodedMessage;
			}
			catch (Exception ex)
			{
				tdText.InnerText = "!!! Chyba pri zaznamenavani zpravy!!!!";
				AddError ("Chyba pri vkladani zpravy do logu! delka=" + message.Length + " - " + ex.ToString ());
			}
			table.AppendChild (tr);
			tr.AppendChild (tdTime);
			tr.AppendChild (tdText);

			bodyNode.AppendChild (table);

			if (bWriteToOutput)
			{
				//System.Diagnostics.Debug.WriteLine (message);
				System.Console.WriteLine (message);
			}

			//check if log text in console, do not try if there's a problem
			if (bWriteToConsole && Logger.logConsole != null && !isProblem)
			{
				Logger.logConsole.ConsoleWriteLine (message);
			}

			if (isProblem)
			{
				AddDebugText (message, style);
			}
		}

		private static void AddDebugText (string message, string style)
		{
			XmlElement table = dbgDoc.CreateElement ("table");
			XmlElement tr = dbgDoc.CreateElement ("tr");
			XmlElement tdTime = dbgDoc.CreateElement ("td");
			XmlAttribute aTime = dbgDoc.CreateAttribute ("class");
			aTime.InnerText = "time";
			tdTime.Attributes.Append (aTime);
			tdTime.InnerText = AppTime.ToString ();
			XmlElement tdText = dbgDoc.CreateElement ("td");
			XmlAttribute aclass = dbgDoc.CreateAttribute ("class");
			aclass.InnerText = style;
			tdText.Attributes.Append (aclass);

			string encodedMessage = System.Web.HttpUtility.HtmlEncode (message);

			try
			{
				tdText.InnerXml = encodedMessage;
			}
			catch (Exception ex)
			{
				tdText.InnerText = "!!! Chyba pri zaznamenavani debug zpravy!!!!";
				AddError ("Chyba pri vkladani zpravy do logu! delka=" + message.Length + " - " + ex.ToString ());
			}
			table.AppendChild (tr);
			tr.AppendChild (tdTime);
			tr.AppendChild (tdText);

			dbgBodyNode.AppendChild (table);
		}

		/// <summary>
		/// saves the whole log as html to log folder 
		/// </summary>
		/// <returns>
		/// A <see cref="System.Boolean"/>
		/// </returns>
		/// <remarks>
		/// this method is thread-safe
		/// </remarks>
		public static bool Save ()
		{
			bool saved = false;
			try
			{
				if (fpss.Count > 10)
				{
					CalculateAverageFPS ();
				}
				AddInternalInfo ("Ukladam...");
				Console.WriteLine ("Ukladani logu do " + savePath);
				string name = "log" + startTime.ToString ("yyyy'-'MM'-'dd'T'HH':'mm':'ss") + ".html";
				name = name.Replace (":", "-");
				lock (doc)
				{
					doc.Save(System.IO.Path.Combine(savePath, "log.html"));
					doc.Save(System.IO.Path.Combine(savePath, name));
#if DEBUG
                    doc.Save("log.html"); 
#endif
					saved = true;
				}
#if DEBUG
                lock (dbgDoc)
                {
                    dbgDoc.Save("debug_log.html");
                } 
#endif
			}
			catch (Exception ex)
			{
				Console.WriteLine ("Chyba ukladani logu!" + ex.ToString ());
			}
			return saved;
		}

		private static void CalculateAverageFPS ()
		{
			if (fpss.Count > 5)
			{
				try
				{
					ulong fpsSum = 0;
					int fpsMax = 0;
					int fpsMin = 10000;
					foreach (int i in fpss)
					{
						fpsSum += (ulong)i;
						if (i > fpsMax)
						{
							fpsMax = i;
						}
						if (i < fpsMin && i > 0)
						{
							fpsMin = i;
						}
					}
					AddImportant ("Prumerny pocet fps je: " + (fpsSum / (ulong)fpss.Count));
					AddImportant ("Max fps = " + fpsMax.ToString () + " Min fps = " + fpsMin.ToString ());
				}
				catch (Exception ex)
				{
					AddError ("Chyba pocitani prumerne hodnoty fps.\n" + ex.ToString ());
					fpss.Clear ();
				}
			}
		}

		/// <summary>
		/// add info to logger with "info" style,
		/// this message is NOT checked for current log level
		/// and is logged always
		/// </summary>
		/// <param name="info">
		/// A <see cref="System.String"/>
		/// </param>
		private static void AddInternalInfo (string info)
		{
			AddText (info, "info");
		}

		/// <summary>
		///add info to logger with "info" style (plain text)
		/// </summary>
		/// <param name="info">
		/// A <see cref="System.String"/>
		/// </param>
		public static void AddInfo (string info)
		{
			if (logLevel <= LogLevel.Info)
			{
				AddText (info, "info");
			}
		}

		/// <summary>
		/// add info to logger with "important" style 
		/// </summary>
		/// <param name="info">
		/// A <see cref="System.String"/>
		/// </param>
		public static void AddImportant (string info)
		{
			AddText (info, "important");
		}
		
		/// <summary>
		///add info to logger with "warning" style 
		/// </summary>
		/// <param name="info">
		/// A <see cref="System.String"/>
		/// </param>
		public static void AddWarning (string info)
		{
			AddText (info, "warning", true);
		}
		/// <summary>
		/// add info to logger with "error" style 
		/// </summary>
		/// <param name="info">
		/// A <see cref="System.String"/>
		/// </param>
		public static void AddError (string info)
		{
			AddText (info, "error", true);
			Save ();
		}
		/// <summary>
		///add exception info to logger with "error" style 
		/// </summary>
		/// <param name="ex">
		/// A <see cref="Exception"/>
		/// </param>
		/// <remarks>
		/// using ex.ToString()
		/// </remarks>
		public static void AddException(Exception ex)
		{
			AddError(ex.ToString());
		}
		
		/// <summary>
		///log started thread to log 
		/// </summary>
		/// <param name="t">
		/// A <see cref="System.Threading.Thread"/> thread to log
		/// </param>
		public static void AddThreadStart (System.Threading.Thread t)
		{
			try
			{
				AddText ("~~Thread info: " + t.Name + "; priority: " + t.Priority + "; state: " + t.ThreadState +
				         " from thread \"" + System.Threading.Thread.CurrentThread.Name + "\" ~~", "info");
			}
			catch (Exception ex)
			{
				AddError ("Chyba pro loggovani threadu\n" + ex.ToString ());
			}
		}

		public static void AddFPSInfo (int fps)
		{
			try
			{
				if (fpss.Count < fpss.Capacity - 1)
				{
					fpss.Add (fps);
				}
				else
				{
					CalculateAverageFPS ();
					fpss.Clear ();
					fpss.Add (fps);
				}
			}
			catch (Exception ex)
			{
				AddWarning (ex.ToString ());
				Save ();
				fpss.Clear ();
			}
		}
		
		/// <summary>
		/// starts stopwatch,log this info
		/// </summary>
		/// <param name="discribe">
		/// A <see cref="System.String"/>
		/// </param>
		public static void StartTimer (string discribe)
		{
			lock (times) {
				AddText ("Stopovani: " + discribe, "duration");
				
				Stopwatch stopwatch = new Stopwatch ();
				stopwatch.Start ();
				stopwatches.Add (stopwatch);
				times.Add (DateTime.Now);
			}
		}
		
		/// <summary>
		/// stops the stopwatch, log the stopwatch time 
		/// </summary>
		/// <param name="discribe">
		/// A <see cref="System.String"/>
		/// </param>
		public static void StopTimer (string discribe)
		{
			try
			{
				lock (times) {
					//string time = (DateTime.Now - times[times.Count - 1]).TotalMilliseconds.ToString();
					string time = stopwatches[stopwatches.Count - 1].ElapsedMilliseconds.ToString ();
					AddText ("Konec stopovani " + discribe + ". Doba trvani: " + time, "duration");
					
					//TODO vyresit definovani stopek, tak aby se neprekryvaly
					times.RemoveAt (times.Count - 1);
					stopwatches.RemoveAt (stopwatches.Count - 1);
				}
			}
			catch (Exception ex)
			{
				AddWarning ("Zkousim zastavit casovac ktery neexistuje. " + ex.ToString ());
			}
		}
		
		/// <summary>
		/// adds informatin about total allocated memory as debug text
		/// </summary>
		public static void AddMemoryInfo()
		{
			string message = string.Format("Total memory [B]: {0}", GC.GetTotalMemory(false).ToString(System.Globalization.NumberFormatInfo.InvariantInfo));
			
			AddInfo(message);
		}
		
		/// <summary>
		/// adds informatin about total allocated memory as debug text with prefix information
		/// </summary>
		/// <param name="prefix">prefix to add before memory info</param>
		public static void AddMemoryInfo(string prefix)
		{
			string message = string.Format("{0} Total memory [B]: {1}", prefix, GC.GetTotalMemory(false).ToString(System.Globalization.NumberFormatInfo.InvariantInfo));
			
			AddInfo(message);
		}

		#region Runtime Detection

		/// <summary>
		/// true if current runtime is mono 
		/// </summary>
		public static bool? RuntimeIsMono {
			get {
				return Type.GetType ("Mono.Runtime") != null;
			}
		}
		#endregion

		/// <summary>
		/// application start time 
		/// </summary>
		public static DateTime StartTime {
			get { return startTime; }
			set { startTime = value; }
		}

		/// <summary>
		/// true to write messages to std output too 
		/// </summary>
        public static bool WriteToOutput
        {
			get { return bWriteToOutput; }
			set { bWriteToOutput = value; }
		}

		/// <summary>
		/// true to write to attached console, if any 
		/// </summary>
        public static bool WriteToConsole
        {
			get { return bWriteToConsole; }
			set { bWriteToConsole = value; }
		}

		/// <summary>
		/// global log level 
		/// </summary>
        public static LogLevel LogLevel
        {
			get { return logLevel; }
			set { logLevel = value; }
		}

		/// <summary>
		/// full path where the logs are saved 
		/// </summary>
        public static string SavePath
        {
			get { return savePath; }
			set { 
				savePath = value; 
				CreateSavePath();
			}
		}
		
		/// <summary>
		/// default log file save path 
		/// </summary>
		/// <returns>
		/// A <see cref="System.String"/>
		/// </returns>
		public static string GetDefaultSavePath()		
		{
			string savePath = System.IO.Path.Combine (System.Environment.GetFolderPath (Environment.SpecialFolder.ApplicationData), "histogram");
			savePath = System.IO.Path.Combine (savePath, "logs");
			
			return savePath;
		}
		
		/// <summary>
		/// executing assembly version 
		/// </summary>
		public static string AssemblyVersion
        {
            get
            {
                return Assembly.GetExecutingAssembly().GetName().Version.ToString();
            }
        }
	}
}
